
/*
 * Copyright (c) 2010 Sergey N Frolov
 * 
 * See LICENSE file
 */

#include "ngx_javascript.h"

/*
 * Get client slot from server pool
 */
ngx_javascript_client_t *ngx_javascript_server_client_slot(ngx_javascript_server_t *server) {
	int i;
	ngx_javascript_client_t *client;
	for (i = 0; i < server->clients_len; i ++) {
		client = server->clients[i];
		if (client->fd < 0) return client;
	}
	if (i < server->clients_max) {
		client = ngx_javascript_calloc(sizeof(ngx_javascript_client_t));
		if (client == NULL) return NULL;
		// n, fd, id, keep, status, event
		client->n = i;
		client->fd = -1;
		// server
		client->server = server;
		// env
		client->env = JS_NewHashTable(0, JS_HashString, ngx_javascript_hash_strcmp, JS_CompareValues, NULL, NULL);
		if (client->env == NULL) {
			ngx_javascript_free(client);
			return NULL;
		}
		// body
		client->body_fd = -1;
		server->clients[i] = client;
		server->clients_len ++;
		return client;
	}
	return NULL;
}

/*
 * Read data from client. Since we use events, read can return 0 in case of disconnect, which is error
 */
int ngx_javascript_client_read(ngx_javascript_client_t *client, char *buf, int len) {
	int r = read(client->fd, buf, len);
	if (r <= 0) return -1;
	return r;
}

/*
 * Write data to client. Since we use events, write can return 0 in case of disconnect, which is error
 */
int ngx_javascript_client_write(ngx_javascript_client_t *client, char *buf, int len) {
	int r = write(client->fd, buf, len);
# if 0
	write(1, buf, len);
# endif
	if (r <= 0) return -1;
	return r;
}

/*
 * Set client socket non-blocking
 */
int ngx_javascript_client_socket_nonblock(int s) {
	int flags;
	flags = fcntl(s, F_GETFL);
	if (flags < 0) return flags;
	flags |= O_NONBLOCK;
	flags = fcntl(s, F_SETFL, flags);
	if (flags < 0) return flags;
	return 0;
}

/*
 * Free memory in client. Close should be called first
 */
void ngx_javascript_client_free(ngx_javascript_client_t *client) {
	if (client->env != NULL) JS_HashTableDestroy(client->env);
	client->env = NULL;
	ngx_javascript_free(client);
}

/*
 * Close client and clean client data
 */
void ngx_javascript_client_close(ngx_javascript_client_t *client) {
	// n, fd, id, keep, status, event
	if (client->wait) {
		client->wait = 0;
		event_del(&client->event);
	}
	close(client->fd);
	client->fd = -1;
	client->id = 0;
	client->keep = 0;
	client->status = 0;
	// output
	while (client->output != NULL) {
		client->endout = client->output->next;
		ngx_javascript_free(client->output);
		client->output = client->endout;
	}
	client->endout = NULL;
	client->position = 0;
	// memory, alloc
	while (client->memory != NULL) {
		client->endmem = client->memory->next;
		ngx_javascript_free(client->memory);
		client->memory = client->endmem;
	}
	client->endmem = NULL;
	// env
	JS_HashTableEnumerateEntries(client->env, ngx_javascript_hash_remove_entry, NULL);
	// input
	if (client->input != NULL) {
		ngx_javascript_free(client->input);
		client->input = NULL;
	}
	client->input_pos = 0;
	client->input_len = 0;
	client->input_pad = 0;
	client->input_dat = 0;
	// body
	if (client->body != NULL) {
		ngx_javascript_free(client->body);
		client->body = NULL;
	}
	if (client->body_fd >= 0) {
		ngx_javascript_client_body(client, 0);
	}
	client->body_pos = 0;
	client->body_type = 0;
	// worker
	if (client->worker != NULL) {
		client->worker->client = NULL;
		client->worker = NULL;
	}
	// javascript
	client->document_root = NULL;
	client->document_root_len = 0;
	client->script_filename = NULL;
	client->script_filename_len = 0;
	client->open_basedir = NULL;
	client->open_basedir_len = 0;
	client->request_uri = NULL;
	client->request_uri_len = 0;
	client->query_string = NULL;
	client->query_string_len = 0;
	client->cookie = NULL;
	client->cookie_len = 0;
	client->content_type = NULL;
	client->content_type_len = 0;
	client->content_length = 0;
}

/*
 * Allocate memory and return pointer to it saving information about this memory in client
 */
void *ngx_javascript_client_allocate(ngx_javascript_client_t *client, int size) {
	ngx_javascript_memory_t *r = ngx_javascript_malloc(size + sizeof(ngx_javascript_memory_t *));
	if (r == NULL) return NULL;
	r->next = NULL;
	if (client->endmem == NULL) {
		client->endmem = client->memory = r;
	} else {
		client->endmem->next = r;
		client->endmem = r;
	}
	return r->data;
}

/*
 * Client socket write event
 */
void ngx_javascript_client_write_event(int fd, short event, void *arg) {
	ngx_javascript_client_t *client = arg;
	int r = ngx_javascript_fcgi_write_responce(client);
	if (r == 0) {
		ngx_javascript_client_close(client);
	} else if (r < 0) {
		ngx_javascript_client_close(client);
	}
}

/*
 * Run separate thread
 * This is called from worker thread
 * Worker is already initialized
 */
void *ngx_javascript_client_execute_thread(void *arg) {
	ngx_javascript_buffer_t *buffer;
	char *output, *header;
	size_t output_len, header_len;
	int r, status;
	size_t p, len, pad;
	
	ngx_javascript_client_t *client = arg;
	JSContext *jcx = client->worker->jcx;
	
	// Begin
	JS_SetContextThread(jcx);
	JS_BeginRequest(jcx);
	JS_EnterLocalRootScope(jcx);
	JS_SetContextPrivate(jcx, client);
	
	// Execute JavaScript
	r = ngx_javascript_worker_execute(client->worker, &status, &output, &output_len, &header, &header_len);
	
# if 0
	fprintf(stderr, "status = %d, output = %s, output_len = %d, header = %s, header_len = %d\n", status, output, output_len, header, header_len);
# endif
	
	// Output
	if (r == 0) {
		// Copy header data to buffers
		len = (header_len > 0x8000) ? 0x8000 : header_len;
		pad = ngx_javascript_fcgi_padding(len);
		buffer = ngx_javascript_calloc(sizeof(ngx_javascript_buffer_t) + len + pad);
		if (buffer != NULL) {
			ngx_javascript_fcgi_make_header(&buffer->head, FCGI_STDOUT, client->id, len, pad);
			memcpy(((char *) buffer) + sizeof(ngx_javascript_buffer_t), header, len);
			buffer->len = sizeof(fcgi_header) + len + pad;
			client->output = client->endout = buffer;
		}
		// Copy output data to buffers
		for (p = 0; p < output_len; p += 0x8000) {
			len = (p + 0x8000) > output_len ? output_len - p : 0x8000;
			pad = ngx_javascript_fcgi_padding(len);
			buffer = ngx_javascript_calloc(sizeof(ngx_javascript_buffer_t) + len + pad);
			if (buffer != NULL) {
				ngx_javascript_fcgi_make_header(&buffer->head, FCGI_STDOUT, client->id, len, pad);
				memcpy(((char *) buffer) + sizeof(ngx_javascript_buffer_t), output + p, len);
				buffer->len = sizeof(fcgi_header) + len + pad;
				if (client->endout == NULL) {
					client->output = client->endout = buffer;
				} else {
					client->endout->next = buffer;
					client->endout = buffer;
				}
			}
		}
		len = pad = 0;
		buffer = ngx_javascript_calloc(sizeof(ngx_javascript_buffer_t) + len + pad);
		if (buffer != NULL) {
			ngx_javascript_fcgi_make_header(&buffer->head, FCGI_STDOUT, client->id, len, pad);
			buffer->len = sizeof(fcgi_header);
			if (client->endout == NULL) {
				client->output = client->endout = buffer;
			} else {
				client->endout->next = buffer;
				client->endout = buffer;
			}
		}
	}
	
	// Finish
	JS_SetContextPrivate(jcx, NULL);
	JS_LeaveLocalRootScope(jcx);
	JS_MaybeGC(jcx);
	// Clean worker. It creates all objects for next execute
	ngx_javascript_worker_close(client->worker);
	ngx_javascript_worker_init(client->worker);
	// And then end request
	JS_EndRequest(jcx);
	JS_ClearContextThread(jcx);
	
	// Detach worker. It is clean and may be used by other clients
	client->worker->client = NULL;
	client->worker = NULL;
	
	// Write should begin after cleaning worker because write might finish very fast and clean worker in main thread
	client->status = NGX_JAVASCRIPT_STATUS_WRITE;
	client->wait = 1;
	event_set(&client->event, client->fd, EV_WRITE|EV_PERSIST, ngx_javascript_client_write_event, client);
	event_add(&client->event, NULL);
	
	return NULL;
}

/*
 * Prepare everything for executing and start execute thread
 * This is called from main thread only
 */
int ngx_javascript_client_execute(ngx_javascript_client_t *client) {
	client->worker = ngx_javascript_server_worker_slot(client->server);
	if (client->worker == NULL) return -1;
	client->worker->client = client;
	client->status = NGX_JAVASCRIPT_STATUS_EXEC;
	if (pthread_create(&client->thread, NULL, ngx_javascript_client_execute_thread, (void *) client) < 0) return -1;
	if (pthread_detach(client->thread)) return -1;
	return 0;
}

/*
 * Client socket read event
 */
void ngx_javascript_client_read_event(int fd, short event, void *arg) {
	ngx_javascript_client_t *client = arg;
	int r = ngx_javascript_fcgi_read_request(client);
	if (r == 0) {
		if (client->wait) {
			client->wait = 0;
			event_del(&client->event);
		}
		if (ngx_javascript_client_execute(client) < 0) ngx_javascript_client_close(client);
	} else if (r < 0) {
		ngx_javascript_client_close(client);
	}
}

int ngx_javascript_client_body(ngx_javascript_client_t *client, int op) {
	char file[PATH_MAX];
	snprintf(file, PATH_MAX, "%s/javascript-%d-%d", client->server->temp_path, client->server->pid, client->n);
	if (op > 0) {
		return client->body_fd = open(file, O_RDWR|O_CREAT|O_TRUNC);
	} else {
		close(client->body_fd);
		unlink(file);
		client->body_fd = -1;
		return 0;
	}
}
